<!--
@license
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<link rel="import" href="settings.html">
<link rel="import" href="event-api.html">
<link rel="import" href="dom-innerHTML.html">
<script>

  Polymer.DomApi = (function() {
    'use strict';

    var Settings = Polymer.Settings;
    var getInnerHTML = Polymer.domInnerHTML.getInnerHTML;

    var nativeInsertBefore = Element.prototype.insertBefore;
    var nativeRemoveChild = Element.prototype.removeChild;
    var nativeAppendChild = Element.prototype.appendChild;

    var dirtyRoots = [];

    var DomApi = function(node) {
      this.node = node;
      if (this.patch) {
        this.patch();
      }
    };

    DomApi.prototype = {

      flush: function() {
        for (var i=0, host; i<dirtyRoots.length; i++) {
          host = dirtyRoots[i];
          host.flushDebouncer('_distribute');
        }
        dirtyRoots = [];
      },

      _lazyDistribute: function(host) {
        // note: only try to distribute if the root is not clean; this ensures
        // we don't distribute before initial distribution
        if (host.shadyRoot && host.shadyRoot._distributionClean) {
          host.shadyRoot._distributionClean = false;
          host.debounce('_distribute', host._distributeContent);
          dirtyRoots.push(host);
        }
      },

      // cases in which we may not be able to just do standard appendChild
      // 1. container has a shadyRoot (needsDistribution IFF the shadyRoot
      // has an insertion point)
      // 2. container is a shadyRoot (don't distribute, instead set
      // container to container.host.
      // 3. node is <content> (host of container needs distribution)
      appendChild: function(node) {
        var distributed;
        this._removeNodeFromHost(node);
        if (this._nodeIsInLogicalTree(this.node)) {
          var host = this._hostForNode(this.node);
          this._addLogicalInfo(node, this.node, host && host.shadyRoot);
          this._addNodeToHost(node);
          if (host) {
            distributed = this._maybeDistribute(node, this.node, host);
          }
        }
        // if not distributing and not adding to host, do a fast path addition
        if (!distributed && !this._tryRemoveUndistributedNode(node)) {
          // if adding to a shadyRoot, add to host instead
          var container = this.node._isShadyRoot ? this.node.host : this.node;
          nativeAppendChild.call(container, node);
          addToComposedParent(container, node);
        }
        return node;
      },

      insertBefore: function(node, ref_node) {
        if (!ref_node) {
          return this.appendChild(node);
        }
        var distributed;
        this._removeNodeFromHost(node);
        if (this._nodeIsInLogicalTree(this.node)) {
          saveLightChildrenIfNeeded(this.node);
          var children = this.childNodes;
          var index = children.indexOf(ref_node);
          if (index < 0) {
            throw Error('The ref_node to be inserted before is not a child ' +
              'of this node');
          }
          var host = this._hostForNode(this.node);
          this._addLogicalInfo(node, this.node, host && host.shadyRoot, index);
          this._addNodeToHost(node);
          if (host) {
            distributed = this._maybeDistribute(node, this.node, host);
          }
        }
        // if not distributing and not adding to host, do a fast path addition
        if (!distributed && !this._tryRemoveUndistributedNode(node)) {
          // if ref_node is <content> replace with first distributed node
          ref_node = ref_node.localName === CONTENT ?
            this._firstComposedNode(ref_node) : ref_node;
          // if adding to a shadyRoot, add to host instead
          var container = this.node._isShadyRoot ? this.node.host : this.node;
          nativeInsertBefore.call(container, node, ref_node);
          addToComposedParent(container, node, ref_node);
        }
        return node;
      },

      /**
        Removes the given `node` from the element's `lightChildren`.
        This method also performs dom composition.
      */
      removeChild: function(node) {
        if (factory(node).parentNode !== this.node) {
          console.warn('The node to be removed is not a child of this node', 
            node);
        }
        var distributed;
        if (this._nodeIsInLogicalTree(this.node)) {
          var host = this._hostForNode(this.node);
          distributed = this._maybeDistribute(node, this.node, host);
          this._removeNodeFromHost(node);
        }
        if (!distributed) {
          // if removing from a shadyRoot, remove form host instead
          var container = this.node._isShadyRoot ? this.node.host : this.node;
          // not guaranteed to physically be in container; e.g. 
          // undistributed nodes.
          if (container === node.parentNode) {
            nativeRemoveChild.call(container, node);
            removeFromComposedParent(container, node);
          }
        }
        return node;
      },

      replaceChild: function(node, ref_node) {
        this.insertBefore(node, ref_node);
        this.removeChild(ref_node);
        return node;
      },

      getOwnerRoot: function() {
        return this._ownerShadyRootForNode(this.node);
      },

      _ownerShadyRootForNode: function(node) {
        if (!node) {
          return;
        }
        if (node._ownerShadyRoot === undefined) {
          var root;
          if (node._isShadyRoot) {
            root = node;
          } else {
            var parent = Polymer.dom(node).parentNode;
            if (parent) {
              root = parent._isShadyRoot ? parent :
                this._ownerShadyRootForNode(parent);
            } else {
             root = null;
            }
          }
          node._ownerShadyRoot = root;
        }
        return node._ownerShadyRoot;

      },

      _maybeDistribute: function(node, parent, host) {
        var nodeNeedsDistribute = this._nodeNeedsDistribution(node);
        var distribute = this._parentNeedsDistribution(parent) ||
          nodeNeedsDistribute;
        if (nodeNeedsDistribute) {
          this._updateInsertionPoints(host);
        }
        if (distribute) {
          this._lazyDistribute(host);
        }
        return distribute;
      },

      _tryRemoveUndistributedNode: function(node) {
        if (this.node.shadyRoot) {
          if (node.parentNode) {
            nativeRemoveChild.call(node.parentNode, node);
          }
          return true;
        }
      },

      _updateInsertionPoints: function(host) {
        host.shadyRoot._insertionPoints =
          factory(host.shadyRoot).querySelectorAll(CONTENT);
      },

      // a node is in a shadyRoot, is a shadyRoot, 
      // or has a lightParent
      _nodeIsInLogicalTree: function(node) {
        return Boolean(node._lightParent || node._isShadyRoot ||
          this._ownerShadyRootForNode(node) ||
          node.shadyRoot);
      },

      // note: a node is its own host
      _hostForNode: function(node) {
        var root = node.shadyRoot || (node._isShadyRoot ?
          node : this._ownerShadyRootForNode(node));
        return root && root.host;
      },

      _parentNeedsDistribution: function(parent) {
        return parent && parent.shadyRoot && hasInsertionPoint(parent.shadyRoot);
      },

      // TODO(sorvell): technically we should check non-fragment nodes for
      // <content> children but since this case is assumed to be exceedingly
      // rare, we avoid the cost and will address with some specific api
      // when the need arises.
      _nodeNeedsDistribution: function(node) {
        return (node.localName === CONTENT) ||
          ((node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) &&
            node.querySelector(CONTENT));
      },

      _removeNodeFromHost: function(node) {
        if (node._lightParent) {
          var root = this._ownerShadyRootForNode(node);
          if (root) {
            root.host._elementRemove(node);
          }
          this._removeLogicalInfo(node, node._lightParent);
        }
        this._removeOwnerShadyRoot(node);
      },

      _addNodeToHost: function(node) {
        var checkNode = node.nodeType === Node.DOCUMENT_FRAGMENT_NODE ?
          node.firstChild : node;
        var root = this._ownerShadyRootForNode(checkNode);
        if (root) {
          root.host._elementAdd(node);
        }
      },

      _addLogicalInfo: function(node, container, root, index) {
        saveLightChildrenIfNeeded(container);
        var children = factory(container).childNodes;
        index = index === undefined ? children.length : index;
        // handle document fragments
        if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
          // NOTE: the act of setting this info can affect patched nodes
          // getters; therefore capture childNodes before patching.
          var c$ = Array.prototype.slice.call(node.childNodes);
          for (var i=0, n; (i<c$.length) && (n=c$[i]); i++) {
            children.splice(index++, 0, n);
            n._lightParent = container;
          }
        } else {
          children.splice(index, 0, node);
          node._lightParent = container;
        }
      },

      // NOTE: in general, we expect contents of the lists here to be small-ish
      // and therefore indexOf to be nbd. Other optimizations can be made
      // for larger lists (linked list)
      _removeLogicalInfo: function(node, container) {
        var children = factory(container).childNodes;
        var index = children.indexOf(node);
        if ((index < 0) || (container !== node._lightParent)) {
          throw Error('The node to be removed is not a child of this node');
        }
        children.splice(index, 1);
        node._lightParent = null;
      },

      _removeOwnerShadyRoot: function(node) {
        // optimization: only reset the tree if node is actually in a root
        var hasCachedRoot = factory(node).getOwnerRoot() !== undefined;
        if (hasCachedRoot) {
          var c$ = factory(node).childNodes;
          for (var i=0, l=c$.length, n; (i<l) && (n=c$[i]); i++) {
            this._removeOwnerShadyRoot(n);
          }
        }
        node._ownerShadyRoot = undefined;
      },

      // TODO(sorvell): This will fail if distribution that affects this
      // question is pending; this is expected to be exceedingly rare, but if
      // the issue comes up, we can force a flush in this case.
      _firstComposedNode: function(content) {
        var n$ = factory(content).getDistributedNodes();
        for (var i=0, l=n$.length, n, p$; (i<l) && (n=n$[i]); i++) {
          p$ = factory(n).getDestinationInsertionPoints();
          // means that we're composed to this spot.
          if (p$[p$.length-1] === content) {
            return n;
          }
        }
      },

      // TODO(sorvell): consider doing native QSA and filtering results.
      querySelector: function(selector) {
        return this.querySelectorAll(selector)[0];
      },

      querySelectorAll: function(selector) {
        return this._query(function(n) {
          return matchesSelector.call(n, selector);
        }, this.node);
      },

      _query: function(matcher, node) {
        node = node || this.node;
        var list = [];
        this._queryElements(factory(node).childNodes, matcher, list);
        return list;
      },

      _queryElements: function(elements, matcher, list) {
        for (var i=0, l=elements.length, c; (i<l) && (c=elements[i]); i++) {
          if (c.nodeType === Node.ELEMENT_NODE) {
            this._queryElement(c, matcher, list);
          }
        }
      },

      _queryElement: function(node, matcher, list) {
        if (matcher(node)) {
          list.push(node);
        }
        this._queryElements(factory(node).childNodes, matcher, list);
      },

      getDestinationInsertionPoints: function() {
        return this.node._destinationInsertionPoints || [];
      },

      getDistributedNodes: function() {
        return this.node._distributedNodes || [];
      },

      /*
        Returns a list of nodes distributed within this element. These can be
        dom children or elements distributed to children that are insertion
        points.
      */
      queryDistributedElements: function(selector) {
        var c$ = this.childNodes;
        var list = [];
        this._distributedFilter(selector, c$, list);
        for (var i=0, l=c$.length, c; (i<l) && (c=c$[i]); i++) {
          if (c.localName === CONTENT) {
            this._distributedFilter(selector, factory(c).getDistributedNodes(),
              list);
          }
        }
        return list;
      },

      _distributedFilter: function(selector, list, results) {
        results = results || [];
        for (var i=0, l=list.length, d; (i<l) && (d=list[i]); i++) {
          if ((d.nodeType === Node.ELEMENT_NODE) &&
            (d.localName !== CONTENT) &&
            matchesSelector.call(d, selector)) {
            results.push(d);
          }
        }
        return results;
      },

      _clear: function() {
        while (this.childNodes.length) {
          this.removeChild(this.childNodes[0]);
        }
      },

      setAttribute: function(name, value) {
        this.node.setAttribute(name, value);
        this._distributeParent();
      },

      removeAttribute: function(name) {
        this.node.removeAttribute(name);
        this._distributeParent();
      },

      _distributeParent: function() {
        if (this._parentNeedsDistribution(this.parentNode)) {
          this._lazyDistribute(this.parentNode);
        }
      }

    };

    Object.defineProperty(DomApi.prototype, 'classList', {
      get: function() {
        if (!this._classList) {
          this._classList = new DomApi.ClassList(this);
        }
        return this._classList;
      },
      configurable: true
    });

    DomApi.ClassList = function(host) {
      this.domApi = host;
      this.node = host.node;
    }

    DomApi.ClassList.prototype = {
      add: function() {
        this.node.classList.add.apply(this.node.classList, arguments);
        this.domApi._distributeParent();
      },

      remove: function() {
        this.node.classList.remove.apply(this.node.classList, arguments);
        this.domApi._distributeParent();
      },

      toggle: function() {
        this.node.classList.toggle.apply(this.node.classList, arguments);
        this.domApi._distributeParent();
      }
    }

    // changes and accessors...
    if (!Settings.useShadow) {

      Object.defineProperties(DomApi.prototype, {

        childNodes: {
          get: function() {
            var c$ = getLightChildren(this.node);
            return Array.isArray(c$) ? c$ : Array.prototype.slice.call(c$);
          },
          configurable: true
        },

        children: {
          get: function() {
            return Array.prototype.filter.call(this.childNodes, function(n) {
              return (n.nodeType === Node.ELEMENT_NODE);
            });
          },
          configurable: true  
        },

        parentNode: {
          get: function() {
            return this.node._lightParent || 
              (this.node.__patched ? this.node._composedParent :
              this.node.parentNode);
          },
          configurable: true  
        },

        firstChild: {
          get: function() {
            return this.childNodes[0];
          },
          configurable: true  
        },

        lastChild: {
          get: function() {
            var c$ = this.childNodes;
            return c$[c$.length-1];
          },
          configurable: true  
        },

        nextSibling: {
          get: function() {
            var c$ = this.parentNode && factory(this.parentNode).childNodes;
            if (c$) {
              return c$[Array.prototype.indexOf.call(c$, this.node) + 1];
            }
          },
          configurable: true  
        },

        previousSibling: {
          get: function() {
            var c$ = this.parentNode && factory(this.parentNode).childNodes;
            if (c$) {
              return c$[Array.prototype.indexOf.call(c$, this.node) - 1];
            }
          },
          configurable: true  
        },

        firstElementChild: {
          get: function() {
            return this.children[0];
          },
          configurable: true  
        },

        lastElementChild: {
          get: function() {
            var c$ = this.children;
            return c$[c$.length-1];
          },
          configurable: true  
        },

        nextElementSibling: {
          get: function() {
            var c$ = this.parentNode && factory(this.parentNode).children;
            if (c$) {
              return c$[Array.prototype.indexOf.call(c$, this.node) + 1];
            }
          },
          configurable: true  
        },

        previousElementSibling: {
          get: function() {
            var c$ = this.parentNode && factory(this.parentNode).children;
            if (c$) {
              return c$[Array.prototype.indexOf.call(c$, this.node) - 1];
            }
          },
          configurable: true  
        },

        // textContent / innerHTML
        textContent: {
          get: function() {
            if (this.node.nodeType === Node.TEXT_NODE) {
              return this.node.textContent;
            } else {
              return Array.prototype.map.call(this.childNodes, function(c) {
                return c.textContent;
              }).join('');
            }
          },
          set: function(text) {
            this._clear();
            if (text) {
              this.appendChild(document.createTextNode(text));
            }
          },
          configurable: true  
        },

        innerHTML: {
          get: function() {
            if (this.node.nodeType === Node.TEXT_NODE) {
              return null;
            } else {
              return getInnerHTML(this.node);
            }
          },
          set: function(text) {
            if (this.node.nodeType !== Node.TEXT_NODE) {
              this._clear();
              var d = document.createElement('div');
              d.innerHTML = text;
              for (var e=d.firstChild; e; e=e.nextSibling) {
                this.appendChild(e);
              }
            }
          },
          configurable: true  
        }

      });

      DomApi.prototype._getComposedInnerHTML = function() {
        return getInnerHTML(this.node, true);
      };


    } else {

      DomApi.prototype.querySelectorAll = function(selector) {
        return Array.prototype.slice.call(this.node.querySelectorAll(selector));
      };

      DomApi.prototype.getOwnerRoot = function() {
        var n = this.node;
        while (n) {
          if (n.nodeType === Node.DOCUMENT_FRAGMENT_NODE && n.host) {
            return n;
          }
          n = n.parentNode;
        }
      };

      DomApi.prototype.getDestinationInsertionPoints = function() {
        var n$ = this.node.getDestinationInsertionPoints();
        return n$ ? Array.prototype.slice.call(n$) : [];
      };

      DomApi.prototype.getDistributedNodes = function() {
        var n$ = this.node.getDistributedNodes();
        return n$ ? Array.prototype.slice.call(n$) : [];
      };

      DomApi.prototype._distributeParent = function() {}

      Object.defineProperties(DomApi.prototype, {

        childNodes: {
          get: function() {
            return Array.prototype.slice.call(this.node.childNodes);
          },
          configurable: true
        },

        children: {
          get: function() {
            return Array.prototype.slice.call(this.node.children);
          },
          configurable: true  
        },

        // textContent / innerHTML
        textContent: {
          get: function() {
            return this.node.textContent;
          },
          set: function(value) {
            return this.node.textContent = value;
          },
          configurable: true  
        },

        innerHTML: {
          get: function() {
            return this.node.innerHTML;
          },
          set: function(value) {
            return this.node.innerHTML = value;
          },
          configurable: true  
        }

      });

      var forwards = ['parentNode', 'firstChild', 'lastChild', 'nextSibling', 
      'previousSibling', 'firstElementChild', 'lastElementChild', 
      'nextElementSibling', 'previousElementSibling'];

      forwards.forEach(function(name) {
        Object.defineProperty(DomApi.prototype, name, {
          get: function() {
            return this.node[name];
          },
          configurable: true  
        });
      });

    }

    var CONTENT = 'content';

    var factory = function(node, patch) {
      node = node || document;
      if (!node.__domApi) {
        node.__domApi = new DomApi(node, patch);
      }
      return node.__domApi;
    };

    Polymer.dom = function(obj, patch) {
      if (obj instanceof Event) {
        return Polymer.EventApi.factory(obj);
      } else {
        return factory(obj, patch);
      }
    };

    // make flush available directly.
    Polymer.dom.flush = DomApi.prototype.flush;

    function getLightChildren(node) {
      var children = node._lightChildren;
      return children ? children : node.childNodes;
    }

    function getComposedChildren(node) {
      if (!node._composedChildren) {
        node._composedChildren = Array.prototype.slice.call(node.childNodes);
      }
      return node._composedChildren;
    }

    function addToComposedParent(parent, node, ref_node) {
      var children = getComposedChildren(parent);
      var i = ref_node ? children.indexOf(ref_node) : -1;
      if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
        var fragChildren = getComposedChildren(node);
        fragChildren.forEach(function(c) {
          addNodeToComposedChildren(c, parent, children, i);
        });
      } else {
        addNodeToComposedChildren(node, parent, children, i);
      }
      
    }

    function addNodeToComposedChildren(node, parent, children, i) {
      node._composedParent = parent;
      if (i >= 0) {
        children.splice(i, 0, node);
      } else {
        children.push(node);
      }
    }

    function removeFromComposedParent(parent, node) {
      node._composedParent = null;
      if (parent) {
        var children = getComposedChildren(parent);
        var i = children.indexOf(node);
        if (i >= 0) {
          children.splice(i, 1);
        }
      }
    }

    function saveLightChildrenIfNeeded(node) {
      // Capture the list of light children. It's important to do this before we
      // start transforming the DOM into "rendered" state.
      //
      // Children may be added to this list dynamically. It will be treated as the
      // source of truth for the light children of the element. This element's
      // actual children will be treated as the rendered state once lightChildren
      // is populated.
      if (!node._lightChildren) {
        var c$ = Array.prototype.slice.call(node.childNodes);
        for (var i=0, l=c$.length, child; (i<l) && (child=c$[i]); i++) {
          child._lightParent = child._lightParent || node;
        }
        node._lightChildren = c$;
      }
    }

    function hasInsertionPoint(root) {
      return Boolean(root._insertionPoints.length);
    }

    var p = Element.prototype;
    var matchesSelector = p.matches || p.matchesSelector ||
        p.mozMatchesSelector || p.msMatchesSelector ||
        p.oMatchesSelector || p.webkitMatchesSelector;

    return {
      getLightChildren: getLightChildren,
      getComposedChildren: getComposedChildren,
      removeFromComposedParent: removeFromComposedParent,
      saveLightChildrenIfNeeded: saveLightChildrenIfNeeded,
      matchesSelector: matchesSelector,
      hasInsertionPoint: hasInsertionPoint,
      ctor: DomApi,
      factory: factory
    };

  })();

</script>
